﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Budget.Core.DomainModel
{
    public abstract class BaseEntity
    {
        public virtual int Id { get; protected set; }


        public override bool Equals(object obj)
        {
            var compareTo = obj as BaseEntity;

            if (ReferenceEquals(this, compareTo))
            {
                return true;
            }

            if (compareTo == null || (this.GetType() != compareTo.GetType() && !IsProxy(compareTo)))
            {
                return false;
            }

            if (this.HasSameNonDefaultIdAs(compareTo))
            {
                return true;
            }

            return this.IsTransient() && compareTo.IsTransient() && this.HasSameObjectSignatureAs(compareTo);
        }

        public override int GetHashCode()
        {
            return this.Id;
        }

        private bool IsProxy(BaseEntity other)
        {
            var myType = this.GetType().Name;
            var otherType = other.GetType().Name;
            return otherType.StartsWith(myType + "Proxy");
        }

        private bool IsTransient()
        {
            return this.Id.Equals(default(int));
        }

        private bool HasSameNonDefaultIdAs(BaseEntity compareTo)
        {
            return !this.IsTransient() && !compareTo.IsTransient() && this.Id.Equals(compareTo.Id);
        }

        private bool HasSameObjectSignatureAs(BaseEntity compareTo)
        {
            var signatureProperties = this.GetTypeSpecificSignatureProperties();

            if ((from property in signatureProperties
                 let valueOfThisObject = property.GetValue(this, null)
                 let valueToCompareTo = property.GetValue(compareTo, null)
                 where valueOfThisObject != null || valueToCompareTo != null
                 where (valueOfThisObject == null ^ valueToCompareTo == null) || (!valueOfThisObject.Equals(valueToCompareTo))
                 select valueOfThisObject).Any())
            {
                return false;
            }

            // If we've gotten this far and signature properties were found, then we can
            // assume that everything matched; otherwise, if there were no signature 
            // properties, then simply return the default bahavior of Equals
            return signatureProperties.Any() || base.Equals(compareTo);
        }

        private IEnumerable<PropertyInfo> GetTypeSpecificSignatureProperties()
        {
            return
                this.GetType().GetProperties().Where(
                    p => Attribute.IsDefined(p, typeof(DomainSignatureAttribute), true));
        }
    }
}